Unity Master Class • August 07, 2026
순수 C# 프로그래밍을 하다가 유니티 엔진에 입문하면 생각보다 큰 차이점에 당황하곤 합니다. 그 이유는 유니티 엔진이 가진 고유한 규칙과 구조 위에서 코드를 작성해야 하기 때문입니다.
오늘은 유니티 엔진 내부에서 코딩할 때 발생하는 구조적 특징과 문제점, 그리고 이를 해결하기 위한 다양한 접근법을 알아보겠습니다.
오늘 이야기는 C#에 익숙해지고 디자인 패턴을 잘 쓰는 분들을 위한 이야기 입니다.
유니티에서 스크립트를 게임 오브젝트에 적용하려면 MonoBehaviour를 상속받아 컴포넌트(Component) 형태로 만들어야 합니다.
컴포넌트는 게임 오브젝트(GameObject)에 완벽히 종속되는 구조를 가집니다.
C# 관점에서 컴포넌트는 인스턴스를 생성할 수 있는 클래스이지만, 유니티 안에서는 독립적인 객체로 다뤄지지 않습니다. 유니티에서의 주체(객체)는 어디까지나 '게임 오브젝트'이며, 컴포넌트는 그 오브젝트에 기능을 추가해 주는 부속품일 뿐입니다.
이러한 구조는 C#과 몇 가지 결정적인 차이점을 만들어냅니다.
Instantiate() 함수로 게임 오브젝트를 생성합니다. 하지만 이 함수는 내부적으로 무거운 연산을 거치기 때문에 남용하기 어렵습니다.그렇다면 유니티에서 게임 오브젝트끼리 데이터를 주고받으려면 어떻게 해야 할까요?
GameObject.Find(), GetComponentInChildren() 등 씬 내부를 검색하여 오브젝트나 컴포넌트를 찾는 방법입니다.
Update() 내에서 사용하면 심각한 성능 저하를 일으킵니다.public / [SerializeField])변수를 public으로 선언하거나 [SerializeField] 어트리뷰트를 붙여 유니티 인스펙터창에서 드래그 앤 드롭으로 직접 연결하는 방식입니다.
GetComponent)자신이 속한 게임 오브젝트 내의 다른 컴포넌트를 가져오는 방식입니다.
Awake나 Start 시점에 캐싱(Caching)해서 사용하는 것이 좋습니다.인스펙터 참조 방식의 숨은 단점
코드만 읽어서는 이 변수가 인스펙터에서 어떤 오브젝트와 연결되어 있는지 한눈에 알기 어렵습니다. 인스펙터 연결이 끊기거나 실수로 잘못 연결된 경우, 코드상 오류가 없어도NullReferenceException이 발생해 버그를 찾기가 까다로워집니다.
순수 C#에서는 static 클래스를 활용하거나 DI(의존성 주입) 프레임워크를 사용해 전역 관리를 쉽게 구현합니다. 하지만 유니티의 컴포넌트는 씬에 종속된 게임 오브젝트에 붙어있어야 하므로, 씬이 전환되면 함께 파괴됩니다.
이 때문에 유니티에서는 싱글톤(Singleton) 패턴을 자주 활용합니다.
public class Manager : MonoBehaviour
{
public static Manager Instance { get; private set; }
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject); // 씬이 바뀌어도 파괴되지 않음
}
else
{
Destroy(gameObject); // 이미 존재한다면 중복 오브젝트 파괴
}
}
}
중복 파괴 비용을 줄이기 위해 단 하나의 싱글톤 루트 게임 오브젝트만 만드는 방식이 있습니다. static bool 플래그로 중복 생성을 막은 뒤, 전역으로 관리할 다른 데이터/매니저 오브젝트들을 이 싱글톤 오브젝트의 자식(Child)으로 넣어두는 것입니다. 자식 오브젝트들 역시 부모를 따라 씬이 넘어가도 파괴되지 않으므로, 깔끔하게 전역 관리 구조를 설계할 수 있습니다.
앞서 언급한 문제점들(씬 종속성, 전역 객체 관리, 메모리 파편화)을 해결하기 위해 유니티는 ScriptableObject(SO)라는 강력한 도구를 제공합니다.
SO는 게임 오브젝트에 붙이지 않는 '에셋 형태의 데이터 객체'입니다. 따라서 씬에 종속되지 않는 독립적인 전역 데이터를 쉽게 만들 수 있습니다.
몬스터의 기본 스탯(HP, 공격력, 이동 속도)이나 아이콘, 효과음 등을 SO로 만들어 둡니다.
메모리 절약 효과: 같은 종류의 몬스터 100마리를 씬에 생성할 때, 각 몬스터 컴포넌트가 동일한 데이터를 중복으로 가지고 있을 필요가 없습니다. 몬스터들은 데이터가 담긴 하나의 SO 에셋을 참조(주소 연결)하기만 하면 되므로 메모리를 대폭 아낄 수 있습니다.
SO에 C# Event를 작성해 두고, 게임 오브젝트들이 이 SO 이벤트를 구독/해제하는 방식입니다.
장점: A 오브젝트가 B 오브젝트를 직접 Find로 찾을 필요 없이, SO 이벤트만 발행하면 연결된 모든 기능이 실행됩니다. 오브젝트 간의 결합도가 완전히 낮아지며, 검색 비용도 0에 가까워집니다.
SO 사용 시 주의할 점 메모리 유지: SO는 한 번 메모리에 로드되면 게임이 종료될 때까지 메모리에 남습니다. 무분별하게 남발하면 메모리 오버헤드가 발생할 수 있습니다.
어드레서블 자원 관리 (Addressables): 너무 많은 SO 데이터가 메모리를 차지하는 것이 걱정된다면, 유니티의 Addressables System을 도입해 사용하지 않는 SO 에셋을 수동으로 메모리에서 해제(Release)해 주는 최적화 기법을 병행하는 것이 좋습니다.
여기까지 유니티 특유의 구조와 그로 인한 최적화 고민들을 다뤄보았습니다. 글이 조금 길고 복잡해 보였을 수 있지만, 기본적인 유니티 코딩 자체는 매우 직관적이고 쉬운 편입니다.
이제 막 유니티를 시작하시는 입문자라면 처음부터 최적화나 메모리 구조에 너무 스트레스받지 마세요.
먼저 C# 기초를 다집니다.
최적화 고민 없이 구현하고 싶은 기능을 자유롭게 만들어 봅니다. (필요하다면 비주얼 스크립팅을 활용해 보는 것도 로직 흐름을 이해하는 데 큰 도움이 됩니다.)
코딩에 익숙해진 뒤, 위에서 다룬 메모리와 연산 비용을 하나씩 줄여가는 최적화에 도전해 보세요.
비용을 줄이고 구조를 깔끔하게 다듬는 최적화 작업은 마치 어려운 퍼즐을 풀어내는 것 같은 색다른 재미를 줍니다.
긴 글 읽어주셔서 감사합니다! 다음에는 입문자분들에게 도움이 될 비주얼 스크립팅(Visual Scripting)에 대한 이야기로 찾아오겠습니다.
현재글: [코딩 주의!]유니티 안에서의 코딩